非中断(正常)模式下的调试

  本书经常使用的一个命令是 Console.WriteLine() 函数,它可以把文本输出到控制台。在开发应用程序时,这个函数可以方便地获得操作的额外反馈,例如:

        Console.WriteLine("MyFunc() Function about to be called");
        MyFunc("Do something.");
        Console.WriteLine("MyFunc() Function execution completed.");

  这段代码说明了如何获取 MyFunc() 函数的额外信息。这么做完全正确,但控制台的输出结果会比较混乱。在开发其他类型的应用程序时,如桌面应用程序,没有用于输出信息的控制台。作为一种替代方法,可将文本输出到另一个位置上--IDE中的 输出 窗口。

  第 2 章简要介绍了 错误列表 窗口,提到其他窗口也可以显示在这个位置上。其中一个窗口就是 输出 窗口,在调试时这个窗口非常有用。要显示这个窗口,可以选择 视图(V) | 输出(O)。在这个窗口中,可以查看与代码的编译和执行相关的信息,包括在编译过程中遇到的错误等,还可以将自定义的诊断信息直接写到这个窗口中。该窗口 如图 7-1 所示

图7-1

图7-1

使用 输出 窗口的下拉菜单可以选择三种模式:生成生成顺序调试。分别显示编译和运行期间的信息。本书提到的 “写入 输出 窗口”时,实际上是指 “写入 输出 窗口的 调试 模式视图”。

  另外,还可以创建一个日志文件,在运行应用程序时,会把信息添加到该日志文件中。把信息写入日志文件所用的技巧与把文本写到 输出 窗口上所用的技巧相同,但需要理解如何从 C#应用程序中访问文件系统。我们把这个功能放在后面的章节中加以讨论,因为目前不必了解文件访问技巧也可以完成许多工作。

   1. 输出调试信息

  在运行期间把文本写入 “输出” 窗口是非常简单的。只要用需要的调用替换 Console.WriteLine() 调用,就可以把文本写到希望的地方。此时可以使用如下两个命令:

   ● Debug.WriteLine()

   ● Trace.WriteLine()

  这两个命令函数的用法几乎完全相同,但有一个重要区别:第一个命令仅在调试模式下运行,而第二个命令还可以用于发布程序。实际上,Debug.WriteLine() 命令甚至不能编译到可发布的程序中,在发布版本中,该命令会消失,这肯定有其优点(编译好的代码文件比较小)。实际上,一个源文件可以创建出两个版本的应用程序。调试版本显示所有的额外诊断信息,而发布版本没有这个开销,也不向用户显示可能导致它们反感的消息。

  这两个函数的用法与 Console.WriteLine() 是不同的。其唯一的字符串参数用于输出消息,而不需要使用 {X} 语法插入变量值。这意味着必须使用 + 串联运算符等方式在字符串中插入变量值。它们还可以有第二个字符串参数(可选),用于显示输出文本的类型,这样,如果应用程序的不同地方输出了类似的消息,我们就可以马上确定 输出 窗口中显示的是哪些输出信息。

  这些函数的一般输出如下所示:

        <category>: <message>

  例如,下面的语句把 MyFunc 作为可选的类别参数:

        Debug.WriteLine("Added1 to i", "Myfunc");

  其结果为:

        MyFunc: Added 1 to i

下面的示例按这种方式输出调试信息。

  修改代码,如下所示:

        using System;
        using System.Collections.Generic;
        using System.Diagnostics;
        using System.Linq;
        using System.Text;
        using System.Threading.Tasks;

        namespace TracePoint
        {
            class Program
            {
                static void Main(string[] args)
                {
                    int[] testArray = { 4, 7, 4, 2, 7, 3, 7, 8, 3, 9, 1, 9 };
                    int[] maxValIndices;
                    int maxVal = Maxima(testArray, out maxValIndices);
                    Console.WriteLine("Maximum value {0} found at element indices:", 
                                        maxVal);
                    foreach (int index in maxValIndices)
                    {
                        Console.WriteLine(index);
                    }
                    Console.ReadKey();
                }

                static int Maxima(int[] integers, out int[] indices)
                {
                    Debug.WriteLine("Maximum value search started.");
                    indices = new int[1];
                    int maxVal = integers[0];
                    indices[0] = 0;
                    int count = 1;
                    Debug.WriteLine(string.Format("Maximum value initialized to {0}, 
                                    at element index 0.", maxVal));
                    for (int i = 1; i < integers.Length; i++)
                    {
                        Debug.WriteLine(string.Format("Now looking at element at index {0}", i));
                        if (integers[i] > maxVal)
                        {
                            maxVal = integers[i];
                            count = 1;
                            indices = new int[1];
                            indices[0] = i;
                            Debug.WriteLine(string.Format("New maximum found. New value is {0}, at element index {1}.", 
                                            maxVal, i));
                        }
                        else
                        {
                            if (integers[i] == maxVal)
                            {
                                count ++;
                                int[] oldIndices = indices;
                                indices = new int[count];
                                oldIndices.CopyTo(indices, 0);
                                indices[count - 1] = i;
                                Debug.WriteLine(string.Format("Duplicate maximum found at element index {0}.", i));
                            }
                        }
                    }

                    Trace.WriteLine(string.Format("Maximum value {0} found, with {1} occurrences.", 
                                    maxVal, count));
                    Debug.WriteLine("Maximum value search completed.");
                    return maxVal;
                }
            }
        }

  在 调试 模式下执行代码,然后中断应用程序的执行,查看 输出 窗口中的内容。

  使用标准工具栏的下拉菜单,切换到 Release 模式。

  再次运行程序,这次在 Release 模式下运行,并在执行中止时再查看一下 输出 窗口。

示例的说明

  这个应用程序是第 6 章中一个示例的扩展版本,它使用一个函数计算整数数组中的最大值。这个版本也返回一个索引数组,表示最大值在数组中的位置,以便调用代码处理这些元素。

        using System.Diagnostics;

  这简化了前面讨论的函数的访问,因为它们包含在 System.Diagnostics 名称空间中,没有这个 using 语句,下面的代码:

        Debug.WriteLine("Bananas");

  就需要进一步予以限定,重新编写这行语句,如下所示:

        System.Diagnostics.Debug.WriteLine("Bananas");

  using 语句使代码更简单,缩短了代码的长度。

  Main() 中的代码仅初始化一个测试用的整数数组 testArray,并声明了另一个整数数组 maxValIndices,以存储 Maxima() (执行计算的函数)的索引输出结果,接着调用这个函数。函数返回后,代码就会输出结果。

  Maxima() 稍微复杂一些,但用到的代码大部分在前面已经看到过。在数组中进行搜索的方式与第 6 章的 MaxVal() 函数类似,但要用一个记录来存储最大值的索引。

  特别需要注意用来跟踪索引的函数(而不是输出调试信息的那些代码行)。Maxima() 并没有返回一个足以存储源数组中每个索引的数组(需要与源数组有相同的维数),而是返回一个正好能容纳搜索到的索引的数组。这可以通过在搜索过程中连续重建不同长度的数组来实现。必须这么做,因为一旦创建好数组,就不能重新设置长度。

  开始搜索时,假定源数组中的第一个元素就是最大值,而且数组中只有一个最大值。因此可以为 maxVal (函数的返回值,即搜索到的最大值)和 indicesout 参数数组,存储搜索到的最大值的索引)设置值。maxVlu 被赋予 integers 中第一个元素的值,indices 被赋予一个值 0, 即数组中第一个元素的索引。在变量 count 中存储搜索到最大值的个数,以便跟踪 indices 数组。

  函数的主体是一个循环,它迭代 integers 数组中的各个值,但忽略第一个值,因为它已经处理过了这个值。每个值都与 maxVal 的当前值进行比较,如果 maxVal 更大,就忽略该值。如果当前处理的值比 maxVal 大,就修改 maxValindices,以反映这种情况。如果当前处理的值与 maxVal 相等,就递增 count,用一个新数组替代 indices。这个新数组比旧 indices 数组多一个元素,包含搜索到的新索引。

  最后一个功能的代码如下所示:

        if (integers[i] == maxVal) 
        { 
            count++; 
            int[] oldIndices = indices; 
            indices = new int[count]; 
            oldIndices.CopyTo(indices, 0); 
            indices[count - 1] = i; 
            Debug.WriteLine(string.Format("Duplicate maximum found at element index {0}.", i)); 
        }

  这段代码把旧 indices 数组备份到 if 代码块的 oldIndices 局部整型数组中。注意 ⚠️使用 <array>.CopyTo() 函数把 oldIndices 中的值复制到新的 indices 数组中。这个函数的参数是一个目标数组和一个用于复制第一个元素的索引,并把所有的值都粘贴到目标数组中。

  在代码中,各个文本部分都使用 Debug.WriteLine()Trace.WriteLine() 函数进行输出,这些函数使用 string.Format() 函数把变量值嵌套在字符串中,其方式与 Console.WriteLine() 相同。这比使用 + 串联运算符更加高效。

  在 Debug 模式下运行应用程序时,其最终结果是一个完整记录,它记述了在循环中计算出结果所采取的步骤。在 Release 模式下,仅能看到计算的最终结果,因为没有调用 Debug.WriteLine() 函数。

  除了这些 WriteLine() 函数外,还需要注意其他一些函数。首先是 Console.Write() 的等价函数:

   ● Debug.Write()

   ● Trace.Write()

  这两个函数使用的语法与 WriteLine() 函数相同(一个或两个参数,即一个消息和可选的类型),但它们是有区别的,因为它们没有添加行尾字符。

  还有下列函数:

   ● Debug.WriteLineIf()

   ● Trace.WriteLineIf()

   ● Debug.WriteIf()

   ● Trace.WriteIf()

  这些函数的参数都与没有 if 的对应函数相同,但增加了一个必选参数,且该参数放在参数列表的最前面。这个参数的值为布尔值(或者计算结果为布尔值的表达式),只有这个值为 true 时,函数才会输出文本。使用这些函数可以根据条件把文本输出到 输出 窗口中。

  例如,只需在某些情况下输出调试信息,所以代码中有许多 Debug.WriteLineIf() 语句,在满足特定的条件后它们才会输出信息。如果没有满足条件,就不显示它们,以防 输出 窗口显示多余的信息。


   2. 跟踪点

  另一种把信息输出到 输出 窗口中的方法是使用跟踪点( tracepoint )。这是VS的一个功能,而不是 C#的功能,但其作用与使用 Debug.WriteLine() 相同。它实际上是输出调试信息且不修改代码的一种方式。

  为了演示跟踪点,可用它们替代上一个示例中的调试命令。添加跟踪点的过程如下:

  (1)把光标放在要插入跟踪点的代码行上。跟踪点会在执行这行代码之前被处理。

  (2)右击该行代码,选择 断点 | 插入跟踪点

  (3)在打开的 命中断点时对话框时,在 打印消息 文本框中键入要输出的字符串。如果要输出变量值,应把变量名放在花括号中。

  (4)单击 OK 按钮。在包含跟踪点的代码行左边会出现一个红色的菱形,该行代码也会突出显示为红色。

  看一下添加跟踪点的对话框标题和所需要的菜单选项,显然,跟踪点是断点的一种形式(可以暂停应用程序的执行,就像断点一样)。断点一般用于更高级的调试目的,本章稍后将介绍断点。

  图7-4 显示了 TracePoints中第 31 行所需的跟踪点。在删除已有的 Debug.WriteLine() 语句后,对代码进行编号。

图7-4 图7-4

如图 7-4 中的文本所示,跟踪点允许插入与跟踪点的位置和上下文相关的其他有用信息。用户应试着使用这些值,尤其是 $FUNCTION 和 $CALLER,看看可以得到什么额外信息。还可以看出,跟踪点可以执行宏,但这是一项高级功能,这里不予以介绍。

  还有一个窗口可用于快速查看应用程序中的跟踪点。要显示这个窗口,可从VS菜单中选择 调试(D) | 窗口(W) | 断点(B)。这是显示断点的通用窗口(如前所述,跟踪点是断点的一种形式)。可以定制显示的内容,从这个窗口的 下拉框中添加 命中条件 列,显示与跟踪点关系更密切的信息。图 7-5 显示的窗口配置了这个列,还显示了添加到 TracePoint 中的所有跟踪点。

图7-5

  在调试模式下执行这个应用程序,会得到与前面完全相同的结果。在代码窗口中右击跟踪点,或者利用 断点 窗口,可以删除或临时禁用跟踪点。在 Breakpoints 窗口中,跟踪点左边的复选框指示是否启用跟踪点;禁用的跟踪点未被选中,在代码窗口中显示为菱形框,而不是实心菱形。

   3. 诊断输出与跟踪点

  前面介绍了两种输出相同信息的方法,下面看看它们的优缺点。首先,跟踪点与 Trace 命令并不等价,也就是说,不能使用跟踪点在发布版本中输出信息。这是因为跟踪点并没有包含在应用程序中。跟踪点由VS处理,在应用程序的已编译版本中,跟踪点是不存在的。只有应用程序运行在VS调试器中时,跟踪点才起作用。

  跟踪点的主要缺点也是其主要优点,即它们存储在VS中,因此可以在需要时便捷地添加到应用程序中,而且也非常容易删除。如果输出非常复杂的信息字符串,觉得跟踪点非常讨厌,只需单击表示其位置的红色菱形,就可以删除跟踪点。

  跟踪点的一个优点是允许方便地添加额外信息,如上一节提到的 &FUNCTION。这个信息可以用 DebugTrace 命令来编写,但比较难。总之,输出调试信息的两种方法是:

   ● 诊断输出: 总是要从应用程序中输出调试结果时使用这种方法,尤其是在要输出的字符串比较复杂,涉及几个变量或许多信息的情况下,使用该方法比较好。另外,如果要在执行发布版本的应用程序的过程中进行输出,Trace 命令经常是唯一的选择。

   ● 跟踪点: 调试应用程序时,如果希望快速输出重要信息,以便消除语义错误,应使用跟踪点。

🔚